package com.googlecode.ebasys.web;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.io.ClassPathResource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.HttpServletBean;

/**
 * An HTTP Servlet that loads static resources from classpath (including jar
 * files). Inspired by {@link org.springframework.web.servlet.ResourceServlet},
 * though it does not use query parameters for getting the resource name, so the
 * request URL looks like: /ebasys/resources/static/stuff.txt instead of
 * /ebasys/resources?resource=static/stuff.txt.
 * 
 * Usage is simple, you should add it to web.xml like this:
 * 
 * <pre>
 * &lt;servlet&gt;
 *   &lt;servlet-name&gt;resources&lt;/servlet-name&gt;
 *   &lt;servlet-class&gt;
 *     com.googlecode.ebasys.web.ClassPathResourceServlet
 *   &lt;/servlet-class&gt;
 *   &lt;init-param&gt;
 *     &lt;param-name&gt;resourceUrlPrefix&lt;/param-name&gt;
 *     &lt;param-value&gt;/ebasys/resources/&lt;/param-value&gt;
 *   &lt;/init-param&gt;
 *   &lt;init-param&gt;
 *     &lt;param-name&gt;allowedResources&lt;/param-name&gt;
 *     &lt;param-value&gt;static/** /*.*&lt;/param-value&gt;
 *   &lt;/init-param&gt;
 *   &lt;load-on-startup&gt;0&lt;/load-on-startup&gt;
 * &lt;/servlet&gt;
 * &lt;servlet-mapping&gt;
 *   &lt;servlet-name&gt;resources&lt;/servlet-name&gt;
 *   &lt;url-pattern&gt;/resources/*&lt;/url-pattern&gt;
 * &lt;/servlet-mapping&gt;
 * </pre>
 * 
 * @author Tomas Varaneckas
 * @version $Id: ClassPathResourceServlet.java 34 2009-01-12 18:17:21Z
 *          tomas.varaneckas $
 */
public class ClassPathResourceServlet extends HttpServletBean {

    /**
     * Serial Version UID
     */
    private static final long serialVersionUID = 8617481698901569080L;

    /**
     * Transfer buffer size
     */
    private static final int BUFFER_SIZE = 16 * 1024;

    /**
     * URL prefix that is stripped from the beginning of request. I.e., if
     * request URI is /ebasys/resources/static/stuff.txt and resourceUrlPrefix
     * is /ebasys/resources/, the Servlet will look for static/stuff.txt in
     * classpath (including jar files).
     */
    private String resourceUrlPrefix;

    /**
     * Pattern with allowed resources. It's Ant style, unless
     * {@link PathMatcher} is overridden in some subclass.
     */
    private String allowedResources;

    /**
     * PathMatcher for validating allowed resources
     */
    private PathMatcher pathMatcher;

    /**
     * Remember the startup time, using no last-modified time before it.
     */
    protected void initServletBean() {
        this.pathMatcher = getPathMatcher();
    }

    /**
     * Return a PathMatcher to use for matching the "allowedResources" URL
     * pattern. Default is AntPathMatcher. Can be overridden in subclasses.
     * 
     * @see #setAllowedResources
     * @see org.springframework.util.AntPathMatcher
     * @return AntPathMatcher
     */
    protected PathMatcher getPathMatcher() {
        return new AntPathMatcher();
    }

    /**
     * Sets allowed resource mask
     * 
     * @see #allowedResources
     * @param allowedResources
     */
    public void setAllowedResources(final String allowedResources) {
        this.allowedResources = allowedResources;
    }

    /**
     * Sets resource prefix
     * 
     * @see #resourceUrlPrefix
     * @param resourceUrlPrefix
     */
    public void setResourceUrlPrefix(final String resourceUrlPrefix) {
        this.resourceUrlPrefix = resourceUrlPrefix;
    }

    /**
     * Determines the resource URI
     * 
     * @param request
     *            current HttpServletRequest
     * @return
     */
    protected String determineResourceUri(final HttpServletRequest request) {
        if (resourceUrlPrefix != null) {
            return request.getRequestURI().replaceFirst(resourceUrlPrefix, "");
        }
        return request.getRequestURI();
    }

    /**
     * Determine the URL of the target resource and include it.
     * 
     * @see #determineResourceUrl
     */
    @Override
    protected final void doGet(final HttpServletRequest request,
            final HttpServletResponse response) throws ServletException,
            IOException {
        final String resourceUri = determineResourceUri(request);
        if (resourceUri != null) {
            // check whether URL matches allowed resources
            if (this.allowedResources != null
                    && !this.pathMatcher.match(this.allowedResources,
                            resourceUri)) {
                throw new ServletException("Resource [" + resourceUri
                        + "] does not match allowed pattern ["
                        + this.allowedResources + "]");
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Including resource [" + resourceUri + "]");
            }
            doInclude(request, response, resourceUri);
        }
    }

    /**
     * Does the actual resource inclusion (simply reads the data from stream and
     * writes it to current {@link HttpServletResponse})
     * 
     * @param request
     *            current HTTP request
     * @param response
     *            current HTTP response
     * @param resourceUri
     *            resource URI in classpath
     * @throws IOException
     *             if file is not found
     */
    private void doInclude(final HttpServletRequest request,
            final HttpServletResponse response, final String resourceUri)
            throws IOException {
        final ClassPathResource res = new ClassPathResource(resourceUri);
        final ReadableByteChannel source = Channels.newChannel(res
                .getInputStream());
        final WritableByteChannel target = Channels.newChannel(response
                .getOutputStream());
        final ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
        while (source.read(buffer) != -1) {
            buffer.flip();
            target.write(buffer);
            buffer.compact();
        }
        buffer.flip();
        while (buffer.hasRemaining()) {
            target.write(buffer);
        }
    }

}
